﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Collections.Specialized;

namespace MLOCore.Utility
{
    // TODO: Need to convert class to inherit from ID3Reader, so we can have a collection of readers, and just interogate the file via extension.
    internal class WMAID3Tag : ID3Reader
    {
        // Converted from VB from: http://www.codeproject.com/KB/files/tagreader.aspx
        // This class will read all the attributes from the ContextBlock and ExtendedContextBlock in a WMA file 
        // It makes available the attributes that are most interesting directly, and allows the retrieval on any string attribute by name. 
        // Could easily be extended to allow the retrieval of non-string attributes, I just didn't need them. 
        // I couldn't find an easy way to have a resumable enumeration over a hash table (there's not items() array) I didn't 
        // implement an enumerator. It wouldn't be hard to do, just clumsy. 
        #region processing
        private struct value
        {
            public Int16 dataType;
            public int index;
            public string Value;
        }

        private class AttributeDictionary : Dictionary<string, value>
        {
        }

        // WMA GUIDs 
        private static readonly Guid hdrGUID = new Guid("75B22630-668E-11CF-A6D9-00AA0062CE6C");
        private static readonly Guid contentGUID = new Guid("75B22633-668E-11CF-A6D9-00AA0062CE6C");
        private static readonly Guid extendedContentGUID = new Guid("D2D0A440-E307-11D2-97F0-00A0C95EA850");


        public ID3Information _ReadFile(string fn)
        {
            AttributeDictionary  attrs = new AttributeDictionary();

            string title = string.Empty;
            string artist = string.Empty;
            string album = string.Empty;
            string genre = string.Empty;
            string copyright = string.Empty;
            string description = string.Empty;
            string rating = string.Empty;
            int year = DateTime.Now.Year;
            int track = 0;
            FileStream stream;
            BinaryReader br;
            List<object> attrValues = new List<object>();

            Guid g;
            bool CBDone = false;
            bool ECBDone = false;
            long sizeBlock;
            string s;
            int i;
            
            //try
            //{
            stream = new FileStream(fn, FileMode.Open, FileAccess.Read);
            br = new BinaryReader(stream);
            //}
            //catch (Exception ex)
            //{
            //    Interaction.MsgBox("Could not open " + fn);
            //    return;
            //}

            g = readGUID(br);
            if (!Guid.Equals(g, hdrGUID))
            {
                // throw an exception 
                throw new Exception("Invalid WMA file format.");
            }
            br.ReadInt64();
            // the size of the entire block 
            br.ReadInt32();
            // the number of entries 
            br.ReadBytes(2);
            // two reserved bytes 
            // Process all the GUIDs until you get both the contextblock and the extendedcontextblock 
            g = readGUID(br);
            while (g != null)
            {
                sizeBlock = br.ReadInt64();
                // this is the size of the block 
                // shouldn't happen, but at least fail gracefully 
                if (br.BaseStream.Position + sizeBlock > stream.Length)
                {
                    break; // TODO: might not be correct. Was : Exit While 
                }
                if (Guid.Equals(g, contentGUID))
                {
                    processContentBlock(br, ref title, ref artist, ref copyright, ref description, ref rating);
                    if (ECBDone)
                    {
                        break; // TODO: might not be correct. Was : Exit While 
                    }
                    CBDone = true;
                }
                else if (Guid.Equals(g, extendedContentGUID))
                {
                    processExtendedContentBlock(br, attrs);
                    if (CBDone)
                    {
                        break; // TODO: might not be correct. Was : Exit While 
                    }
                    ECBDone = true;
                }
                else
                {
                    // not one we're interested in, skip it 
                    sizeBlock -= 24;
                    // already read the guid header info 
                    if (sizeBlock + br.BaseStream.Position >= 0)
                        br.BaseStream.Position += sizeBlock;
                }

                g = readGUID(br);

            }

            // Get the attributes we're interested in 
            album = getStringAttribute("WM/AlbumTitle", attrs);
            genre = getStringAttribute("WM/Genre", attrs);
            s = getStringAttribute("WM/Year", attrs);
            
            if (int.TryParse(s, out i))
            {
                year = i;
            }
            s = getStringAttribute("WM/TrackNumber", attrs);
            // could be n/<total> 
            i = s.IndexOf("/");
            if (!(i == -1))
            {
                s = s.Substring(0, i);
            }
            if (int.TryParse(s, out i))
            {
                track = i;
            }
            else
            {
                s = getStringAttribute("WM/Track", attrs);
                i = s.IndexOf("/");
                if (!(i == -1))
                {
                    s = s.Substring(0, i);
                }
                if (int.TryParse(s, out i))
                {
                    track = i;
                }
            }

            return new ID3Information(artist, album, title, genre, description, year, track);
        }
        private string readUnicodeString(BinaryReader br, Int16 len)
        {
            //Can't use .NET functions, since they expect the length field to be a single byte for strings < 256 chars 
            char[] ch = new char[len - 2];
            int i;
            short k;

            for (i = 0; i < len - 2; i++)
            {
                k = br.ReadInt16();
                ch[i] = Convert.ToChar((int)(k & 0xffff)); ;
            }
            k = br.ReadInt16();
            return new string(ch);
        }
        private string readUnicodeString(BinaryReader br)
        {
            Int16 datalen;
            Int16 len;
            //Can't use .NET functions, since they expect the length field to be a single byte for strings < 256 chars 
            datalen = br.ReadInt16();
            len = Convert.ToInt16( datalen / 2);
            // length in Unicode characters 
            return readUnicodeString(br, len);

        }

        private void processExtendedContentBlock(BinaryReader br, AttributeDictionary attrs)
        {
            Int16 numAttrs;
            Int16 dataType;
            Int16 dataLen;
            Int16 sValue;
            string attrName;
            string strValue;
            byte[] bValue;
            bool boolValue;
            int i;
            int iValue;
            int index = 0;
            long lValue;
            value valueObject;

            numAttrs = br.ReadInt16();
            for (i = 0; i <= numAttrs - 1; i++)
            {
                attrName = readUnicodeString(br);
                dataType = br.ReadInt16();
                switch (dataType)
                {
                    case 0:
                        strValue = readUnicodeString(br);
                        valueObject.dataType = 0;
                        valueObject.index = index;
                        valueObject.Value = strValue;
                        attrs.Add(attrName, valueObject);
                        //attrValues.Add(strValue);
                        index += 1;
                        break;

                    case 1:
                        dataLen = br.ReadInt16();
                        // ERROR: Not supported in C#: ReDimStatement 
                        bValue = br.ReadBytes(dataLen);
                        valueObject.dataType = 1;
                        valueObject.index = index;
                        valueObject.Value = Convert.ToString(bValue);
                        attrs.Add(attrName, valueObject);
                        //attrValues.Add(bValue);
                        index += 1;
                        break;

                    case 2:
                        dataLen = br.ReadInt16();
                        iValue = br.ReadInt32();
                        if (iValue == 0)
                        {
                            boolValue = false;
                        }
                        else
                        {
                            boolValue = true;
                        }

                        valueObject.dataType = 2;
                        valueObject.index = index;
                        valueObject.Value = boolValue.ToString();
                        attrs.Add(attrName, valueObject);
                        //attrValues.Add(boolValue);
                        index += 1;
                        break;

                    case 3:
                        dataLen = br.ReadInt16();
                        iValue = br.ReadInt32();
                        valueObject.dataType = 3;
                        valueObject.index = index;
                        valueObject.Value = iValue.ToString();
                        attrs.Add(attrName, valueObject);
                        //attrValues.Add(iValue);
                        index += 1;
                        break;

                    case 4:
                        dataLen = br.ReadInt16();
                        lValue = br.ReadInt64();
                        valueObject.dataType = 4;
                        valueObject.index = index;
                        valueObject.Value = lValue.ToString();
                        attrs.Add(attrName, valueObject);
                        //attrValues.Add(lValue);
                        index += 1;
                        break;

                    case 5:
                        dataLen = br.ReadInt16();
                        sValue = br.ReadInt16();
                        valueObject.dataType = 5;
                        valueObject.index = index;
                        valueObject.Value = sValue.ToString();
                        attrs.Add(attrName, valueObject);
                        //attrValues.Add(sValue);
                        index += 1;
                        break;

                    default:
                        throw new Exception("Bad value for datatype in Extended Content Block. Value = " + dataType);
                }
            }
        }
        private void processContentBlock(BinaryReader br, ref string title, ref string artist, ref string copyright, ref string description, ref string rating)
        {
            short lTitle;
            short lAuthor;
            short lCopyright;
            short lDescription;
            short lRating;
            short i;

            lTitle = br.ReadInt16();
            lAuthor = br.ReadInt16();
            lCopyright = br.ReadInt16();
            lDescription = br.ReadInt16();
            lRating = br.ReadInt16();
            if (lTitle > 0)
            {
                i = Convert.ToInt16(lTitle / 2);
                title = readUnicodeString(br,i);
            }
            if (lAuthor > 0)
            {
                i = Convert.ToInt16(lAuthor / 2);
                artist = readUnicodeString(br, i);
            }
            if (lCopyright > 0)
            {
                i = Convert.ToInt16(lCopyright / 2);
                copyright = readUnicodeString(br, i);
            }
            if (lDescription > 0)
            {
                i = Convert.ToInt16(lDescription / 2);
                description = readUnicodeString(br, i);
            }
            if (lRating > 0)
            {
                i = Convert.ToInt16(lRating / 2);
                rating = readUnicodeString(br, i);
            }
            
        }

        private Guid readGUID(BinaryReader br)
        {
            int int1;
            short shrt1;
            short shrt2;
            byte[] b = new byte[7];

            try
            {
                int1 = br.ReadInt32();
                if (int1 == -1)
                {
                    return Guid.Empty;
                }
                shrt1 = br.ReadInt16();
                shrt2 = br.ReadInt16();
                b = br.ReadBytes(8);
                Guid g = new Guid(int1, shrt1, shrt2, b);
                return g;
            }
            catch (Exception ex)
            {
                throw new Exception("Invalid WMA format.", ex);
            }
        }
        private string getStringAttribute(string name, AttributeDictionary attrs)
        {
            value v;

            if (!attrs.ContainsKey(name))
            {
                return "";
            }
            v = attrs[name];
            if (!(v.dataType == 0))
            {
                // it's not a string type 
                return string.Empty;
            }
            else
            {
                return (string)v.Value;
            }
        }
#endregion

        internal override bool CanProcessFile(string fileName)
        {
            if (System.IO.Path.GetExtension(fileName).ToLower() == ".wma")
                return true;
            else
                return false;
        }

        internal override ID3Information ProcessFile(string fileName)
        {
            return _ReadFile(fileName);
        }
    }
}
